#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#   pyflowctrl.core4 (prototype), based on experiments/core4.1-stream.py
#
#   Changes:
#   20120128 added Stream types (input,output,debug)
#   20120128 move process statuses to ProcessStatus 
#   0000/00/00 Packet.dump(), use __dict__.copy() instead copy.deepcopy()


__author__ = 'Andrey Usov <http://devel.ownport.net>'
__meta__ = { 'library': 'pyflowctrl', }

import sys
import copy
import collections

class EmptyStream(Exception): pass
class DataTypeInconsistent(Exception): pass

# Process Status
class ProcessStatus():
    ''' process status '''
    not_started     = 0
    waiting         = 1
    processing      = 2
process_status = ProcessStatus()    

# Stream Type
class StreamType():
    ''' stream type '''
    input       = 0
    output      = 1
    debug       = 2
stream_type = StreamType()    


class Packet(object):
    ''' Information packet with field namespace support, 
    based on experiments/packet2.py '''
    
    def __init__(self, **fields):
        ''' init packet '''
        super(Packet, self).__init__()
        self.__namespaces = {}
        
        for fname in fields:
            setattr(self, fname, fields[fname])
    
    def __getattr__(self, name):
        ''' return only "public" fields '''
        if name.startswith('_'):
            raise AttributeError
        else:
            try:
                value = self.__dict__[name]
            except KeyError, err:
                raise AttributeError(err)
            return value

    def __getitem__(self, namespace):
        ''' return sub-packet for specific namespace '''
        if namespace in self.__namespaces:
            return self.__namespaces[namespace]
        else:
            raise AttributeError(namespace)

    def namespaces(self):
        ''' return list of namespaces '''
        return self.__namespaces.keys()

    def set_namespace(self, namespace):
        ''' set namespace '''
        if namespace not in self.__namespaces:
            self.__namespaces[namespace] = Packet()

    def del_namespace(self, namespace):
        ''' delete namespace '''
        if namespace in self.__namespaces:
            del self.__namespaces[namespace]
        else:
            raise AttributeError(namespace)
    
    def copy(self):
        ''' return deepcopy of packet '''
        return copy.deepcopy(self)
    
    def dump(self):
        ''' return dictionary of packet '''
        #packet_dump = copy.deepcopy(self.__dict__)
        packet_dump = self.__dict__.copy()
        if '_Packet__namespaces' in packet_dump:
            # rename '_Packet__namespaces' to '__namespaces'
            packet_dump['__namespaces'] = packet_dump['_Packet__namespaces']
            del packet_dump['_Packet__namespaces']
            # dump namespaces
            for ns in packet_dump['__namespaces']:
                if isinstance(packet_dump['__namespaces'][ns], Packet):
                    packet_dump['__namespaces'][ns] = packet_dump['__namespaces'][ns].dump()
        return packet_dump
    
class Stream(object):
    ''' Stream '''
    
    __STREAM__ = {}
    
    def __init__(self, handler=None):
        ''' __init__ '''
        super(Stream, self).__init__()
        self.__buffer = collections.deque()
        self.__packet_handler = handler
                    
    def get(self):
        ''' get data from stream '''
        try:
            result = self.__buffer.popleft()
        except IndexError:
            raise EmptyStream
        return result
    
    def put(self, packet):
        ''' put packet in stream '''
        if self.__packet_handler:
            packet = self.__packet_handler(packet)
        self.__buffer.append(packet)

class Process(object):
    ''' Process '''
    
    __PROCESS__ = {
                    'name': 'Process',
                    'description': 'Basic process',
                    'author': '',
                    'url': '',
                    'depends on': '',
                    'io': {},
                }
    
    def __init__(self, name=''):
        self.pid = None             # process id
        self.name = ''
        self.status = process_status.not_started
        self.running = self.main()
        
        self.io = {}
        for stream in self.__PROCESS__['io']:
            self.io[stream] = Stream()
    
    def main(self):
        ''' main loop '''
        while True:
            yield process_status.not_started

    def run_once(self, packet=None):
        ''' wrapper for main loop 
        
        if packet is specified run_once return the result of processing
        if packet is not specified run_once return the status of process
        '''
        if packet:
            # TODO handle situation when Process doesn't have input or output streams
            self.io['input'].put(packet)
            self.running.next()
            return self.io['output'].get()
        else:
            return self.running.next()


class ProcessFlow(object):
    # ProcessFlow
    
    # TODO access to process in flow via ProcessFlow[<process_name>]
    
    __PROCESSFLOW__ = {}
    
    def __init__(self):
        self.links = []
        self.pmap = {}          # process map
        self.streams = {}       # merged process streams
    
    def upload(self, network):
        ''' upload process flow network '''
        try:
            self.pmap = network['processes']
            for connection in network['links']:
                source_stream, target_stream, handler = connection
                self.link(source_stream, target_stream, handler)
        except KeyError, err:
            print 'Error! Please specify %s data' % err
            sys.exit()
                
    def link(self, source_stream, target_stream, packet_handler=None):
        ''' create link between processes '''
        if (source_stream, target_stream) not in self.streams:
        
            source, source_output = source_stream.split('.')
            target, target_input = target_stream.split('.')
            self.streams[(source_stream, target_stream)] = Stream(packet_handler)
            
            self.pmap[source].__dict__[source_output] = self.streams[(source_stream, target_stream)]
            self.pmap[target].__dict__[target_input] = self.streams[(source_stream, target_stream)]
            
    def run(self):
        ''' run flow '''
        total_procs = len(self.pmap)
        stopped_procs = 0
        while True:
            if stopped_procs == total_procs:
                break
                
            # every cicle of process flow the number of waiting processes is 0
            waiting_procs = 0
            
            for pname in self.pmap:
                if self.pmap[pname] is None:
                    continue
                try:
                    if self.pmap[pname].run_once() == process_status.waiting:
                        waiting_procs += 1
                except StopIteration:
                    self.pmap[pname] = None
                    stopped_procs += 1
            
            # if all processes are waiting data -> stop process flow
            if waiting_procs == total_procs:
                break




